Skip to content

Use HTML Tag Processor to audit blocking scripts & styles in Site Health’s enqueued-assets test #2059

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 36 commits into
base: trunk
Choose a base branch
from

Conversation

b1ink0
Copy link
Contributor

@b1ink0 b1ink0 commented Jun 18, 2025

Summary

Fixes #2030

Relevant technical choices

This PR overhauls the Site Health "Enqueued Scripts" and "Enqueued Styles" tests to accurately detect and report only truly render-blocking scripts and styles, whether loaded from external files or defined inline. It achieves this by performing an unauthenticated loopback request and analyzing the resulting front-end HTML with the WP_HTML_Tag_Processor.

@b1ink0 b1ink0 added this to the performance-lab n.e.x.t milestone Jun 18, 2025
@b1ink0 b1ink0 requested a review from westonruter June 18, 2025 19:27
@b1ink0 b1ink0 added [Type] Bug An existing feature is broken [Plugin] Performance Lab Issue relates to work in the Performance Lab Plugin only labels Jun 18, 2025
Comment on lines 114 to 117
$path = perflab_aea_get_path_from_resource_url( $href );
if ( '' === $path ) {
continue;
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, our audit only scans assets located in the WordPress content directory. This raises an question,
how should the audit treat scripts and styles served from a CDN? Should these third-party resources be included in the render-blocking report, excluded entirely, or perhaps flagged separately so we can distinguish external blocking assets from local ones?

I think we’ll also need to consider how to measure their sizes efficiently. One approach could be, sending an HTTP HEAD request to the CDN-hosted URL to check for a Content‑Length header.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From a performance perspective, CDN-served assets could be even worse for performance since they require a new TCP connection, now that browsers don't reuse cached resources across origins. So we definitely should be including them in the render-blocking report.

Sending an HTTP HEAD request for all resources regardless of whether they are on the same origin or not makes sense to me. If the request returns in a 404 then this would be important to report as well.

Once we have the report, then a future enhancement would be digging in to find the theme/plugin responsible for the resource being added in the first place. The AMP plugin implements a lot of this, and it was getting extracted a separate package via https://github.com/GoogleChromeLabs/wp-origination but that effort got stalled and was abandoned. See also #1095.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we consider a case where a HEAD request does not return a content length due to server configuration? If so, should we then make a GET request?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps, but that might be overkill.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, sounds good. In that case, let's not do the HEAD request at all and only do GET. The assets should all be relatively small (a few hundred KB at maximum), so it shouldn't be a problem to just go ahead and download them to check the byte size of the body.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in aa50fe4.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sending an HTTP HEAD request for all resources regardless of whether they are on the same origin or not makes sense to me. If the request returns in a 404 then this would be important to report as well.

Now that the GET request is sent, should only 404 errors be added to the report, or should any errors that occur during the request be added to the report?

Copy link
Contributor Author

@b1ink0 b1ink0 Jul 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once we have the report, then a future enhancement would be digging in to find the theme/plugin responsible for the resource being added in the first place. The AMP plugin implements a lot of this, and it was getting extracted a separate package via https://github.com/GoogleChromeLabs/wp-origination but that effort got stalled and was abandoned. See also #1095.

So does it make sense to add a table in the blocking scripts/styles site health test showing the origin of each blocking asset, or should this be part of Optimization Detective as you mentioned in #1095?

cc: @westonruter

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is related to #2059 (comment).

I think instead of just saying whether the sum of of the bytes for blocking assets is above a given threshold, that it would be better to list out the assets in a table with a sum at the end. It wouldn't necessarily be able to identify the theme/plugin responsible for adding the script or stylesheet, but in cases where the script/style is bundled with the theme/plugin then this would be obvious.

Comment on lines 110 to 112
if ( ! is_string( $href ) || false !== strpos( $href, 'wp-includes' ) ) {
continue;
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We currently skip any URL containing wp-includes (i.e. core assets). Since the goal is to surface all render-blocking resources, should we remove that exclusion and include core scripts and styles in the audit as well?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this exclusion should be removed, yes.

Comment on lines 80 to 86
// Process blocking inline scripts.
if ( ! is_string( $src ) ) {
$script_size = mb_strlen( $processor->get_modifiable_text(), '8bit' );
if ( false !== $script_size ) {
$assets['scripts'][] = array(
'src' => 'inline',
'size' => $script_size,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently each inline SCRIPT is reported as its own entry, do we want to continue treating every inline script separately, or would it make sense to aggregate inline scripts into a single summary?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think inline scripts need to be counted since the render blocking is not significant compared to blocking external scripts.

Copy link

codecov bot commented Jun 20, 2025

Codecov Report

Attention: Patch coverage is 54.23729% with 108 lines in your changes missing coverage. Please review.

Project coverage is 66.33%. Comparing base (ff5cc68) to head (e44ee9f).
Report is 75 commits behind head on trunk.

Files with missing lines Patch % Lines
...ludes/site-health/audit-enqueued-assets/helper.php 33.54% 107 Missing ⚠️
...cludes/site-health/audit-enqueued-assets/hooks.php 98.66% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##            trunk    #2059      +/-   ##
==========================================
- Coverage   68.03%   66.33%   -1.71%     
==========================================
  Files          92       93       +1     
  Lines        7631     7863     +232     
==========================================
+ Hits         5192     5216      +24     
- Misses       2439     2647     +208     
Flag Coverage Δ
multisite 66.33% <54.23%> (-1.71%) ⬇️
single 36.00% <47.45%> (-1.02%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

b1ink0 added 3 commits June 24, 2025 10:58
if (
! is_admin() ||
! current_user_can( 'view_site_health_checks' ) ||
( false !== get_transient( 'aea_enqueued_front_page_scripts' ) && false !== get_transient( 'aea_enqueued_front_page_styles' ) )
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would make sense to combine these two transients into one aea_blocking_assets, or something like that

b1ink0 and others added 4 commits June 26, 2025 15:26
…rt HEAD request
Co-authored-by: Weston Ruter <[email protected]>
…page loads

Co-authored-by: Weston Ruter <[email protected]>
…assets` transient

Co-authored-by: Weston Ruter <[email protected]>
Comment on lines 297 to 299
if ( false !== strpos( $resource_url, '?' ) ) {
$resource_url = substr( $resource_url, 0, strpos( $resource_url, '?' ) );
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if ( false !== strpos( $resource_url, '?' ) ) {
$resource_url = substr( $resource_url, 0, strpos( $resource_url, '?' ) );
}
$resource_url = preg_replace( '/\?.*/', '', $resource_url );

Nevertheless, the perflab_aea_get_path_from_resource_url() function isn't being used now anymore, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes its not being used anymore I just put comment about it here #2059 (comment).

b1ink0 and others added 10 commits June 27, 2025 00:58
Co-authored-by: Weston Ruter <[email protected]>
…s are now resolved via HEAD requests
…urement
…ction
@b1ink0 b1ink0 marked this pull request as ready for review July 2, 2025 21:14
Copy link

github-actions bot commented Jul 2, 2025

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: b1ink0 <[email protected]>
Co-authored-by: westonruter <[email protected]>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@b1ink0 b1ink0 requested a review from westonruter July 2, 2025 21:14
Comment on lines 302 to 304
$body = wp_remote_retrieve_body( $response );
if ( '' === $body ) {
return null;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could it be that the body length is actually zero in this case?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m not understanding what you’re trying to suggest. If you’re referring to cases when an asset has no content and the server returns an empty string, this will ignore such assets. Should these assets still be included in the asset count?

Copy link
Member

@westonruter westonruter Jul 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but the issue is that a zero-size body would still be a blocking request.

So I think this condition should be removed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 0be6561.

b1ink0 and others added 4 commits July 3, 2025 19:52
Co-authored-by: Weston Ruter <[email protected]>
Co-authored-by: Weston Ruter <[email protected]>
Co-authored-by: Weston Ruter <[email protected]>
Copy link
Member

@westonruter westonruter left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the delay in reviewing.

if ( 0 === strpos( $resource_url, content_url() ) ) {
return WP_CONTENT_DIR . substr( $resource_url, strlen( content_url() ) );
if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
return null;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if instead of returning null here, it instead returns the WP_Error instance if that's what $response is, or else to return a new WP_Error if the response code is not 200. This could be displayed in the UI then, instead of silently ignoring it. If there is a script on the page that is pointing to a 404, then this would be very important to capture since (1) it would still be a potentially blocking request, (2) a 404 would probably be served by WP which would be relatively very slow compared to serving a static asset.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For this, does it make sense to show a table of failed requests with some info about them, or a general list saying there is an issue with the following assets?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, in fact I think it would be good to have a table listing out each blocking asset, including whether it is a script or stylesheet, the URL of the asset, and the size. When there is an error obtaining the asset, this can be indicated in the table for that row.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 6b23854.

image

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any suggestions on UI @westonruter ?

continue;
}
if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
return;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A failure here should result in some message in the Site Health test, no?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have added a new Site Health test in a77ab19, which will be triggered when the response is a WP_Error, the response code is not equal to 200, or the body is empty.

I think adding this new test and omitting the other tests is better, as it does not make sense to show two failed tests with the similar messages.

WP_Error:
image

Response code is not equal to 200:
image

Response code is 200 but body is empty:
image

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would this be a new test and not just a unique status for the existing test?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are two tests: one for styles and one for scripts. When the request to the home page fails, both of these tests will be omitted in the previous logic. There are two ways to display the error: one is to show the message on one of the existing tests, and the other is to show it in a different, new test. In my opinion, the second seems correct. I am open to any new suggestions for other approaches.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I think the two test styles and scripts can be combined into one asset test, and the failure will be one of the cases in it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, OK. Yes, what you have makes sense.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another option would be for there to be just one test for both scripts and styles. This test would be for the "blocking assets" rather than two separate tests for "blocking scripts" and "blocking styles". I don't feel strongly either way.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, combining them would work.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 93e30ac.

}
$html = wp_remote_retrieve_body( $response );
if ( '' === $html ) {
return;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto. If the HTML response is empty, then this here would indicate a problem with the test that could be exposed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

b1ink0 and others added 5 commits July 7, 2025 14:06
Co-authored-by: Weston Ruter <[email protected]>
Co-authored-by: Weston Ruter <[email protected]>
…s site health test
b1ink0 added 2 commits July 10, 2025 15:04
…rmation
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Plugin] Performance Lab Issue relates to work in the Performance Lab Plugin only [Type] Bug An existing feature is broken
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Site Health tests for enqueued scripts and styles are inaccurate
2 participants